package com.ruoyi.common.filter.filter;

import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.Sign;
import cn.hutool.crypto.asymmetric.SignAlgorithm;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.parser.Feature;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.filter.config.AppSecretProperties;
import org.javatuples.Pair;
import com.ruoyi.common.filter.config.SignVerifynProperties;
import com.ruoyi.common.filter.constants.GatewayConstant;
import com.ruoyi.common.filter.exception.*;
import com.ruoyi.common.filter.recode.CommonReturnCode;
import com.ruoyi.common.filter.util.BodyReaderHttpServletRequestWrapper;
import com.ruoyi.common.filter.util.RequestUtils;
import com.ruoyi.common.filter.util.SignUtils;


import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;

/**
 * @description:
 * @author: Sunhongchen
 * @date: 2021-08-16
 */
@Slf4j
@Component
public class SignAuthFilter implements Filter {
    @Autowired
    private SignVerifynProperties signVerifynProperties;

    @Autowired
    private AppSecretProperties appSecretProperties;

    // 令牌自定义标识
    @Value("${token.header}")
    private String header;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // 防止流读取一次后就没有了, 所以需要将流继续写出去
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletRequest requestWrapper;
        String mediaType = request.getContentType();
        // 获取请求的路径
        String requestUri = request.getRequestURI();
        //requestUri = "/platform" + requestUri;
        String httpMethod = request.getMethod();
        //1.处理验签规则
        //1.0.检查请求地址的格式
        if (requestUri.contains("//")) {
            SignUtils.getException(request, response, CommonReturnCode.GATEWAY_REQUEST_URI_FORMAT);
            return;
        }
        if (mediaType != null && mediaType.contains(MediaType.MULTIPART_FORM_DATA_VALUE)) {
            CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
            requestWrapper = multipartResolver.resolveMultipart(request);
        } else {
            requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
        }
        // 打印日志
        requestLogMessage(requestUri, httpMethod, mediaType, requestWrapper);
        //1.处理验签规则
        //1.1.处理验签白名单
        if (handleSignWhitelist(httpMethod, requestUri, mediaType)) {
            filterChain.doFilter(requestWrapper, response);
            return;
        }
        //1.2.获取并验证Header中的参数，是否满足验签所需
        TreeMap<String, Object> headers;
        try {
            headers = this.getVerifyNecessaryHeader(request);
        } catch (MissingHeaderException e) {
            SignUtils.getException(request, response, CommonReturnCode.GATEWAY_HEADER_PARAMS_ABSENT);
            return;
        }
        //1.3.验证不同验签方式的正确性
        try {
            this.handleSignRule(httpMethod, headers, requestUri);
        } catch (SignRuleException e) {
            SignUtils.getException(request, response, e.getReturnCode());
            return;
        }
        //2.处理验签参数
        Pair<String, TreeMap<String, Object>> pair;
        try {
            pair = this.getVerifyParams(requestWrapper, requestUri);
        } catch (IOException | RequestBodyFormatException e) {
            SignUtils.getException(request, response, CommonReturnCode.REQ_JSON_FORMAT_ERROR);
            return;
        }
        requestUri = pair.getValue0();
        TreeMap<String, Object> params = pair.getValue1();
        //3.处理验签
        boolean isVerify = false;
        try {
            isVerify = verifySign(requestUri, params, headers,request);
        } catch (ValidateTokenException e) {
            SignUtils.getException(request, response, CommonReturnCode.GATEWAY_TOKEN_ABSENT);
            return;
        } catch (ValidatePublicKeyException e) {
            SignUtils.getException(request, response, CommonReturnCode.GATEWAY_SIGN_PARAMS_ABSENT);
            return;
        }
        if (!isVerify) {
            // 验签错误
            SignUtils.getException(request, response, CommonReturnCode.GATEWAY_CHECK_SIGN_FAIL);
            return;
        }

        filterChain.doFilter(requestWrapper, response);
    }

    /**
     * 处理验签白名单
     *
     * @param requestUri
     * @return
     */
    private boolean handleSignWhitelist(String method, String requestUri, String mediaType) {
        if (inWhitelist(method, requestUri, signVerifynProperties.getSkipUrls())) {
            return true;
        }
        return false;
    }

    /**
     * 判断请求地址是否存在于白名单中
     *
     * @param requestUri
     * @return
     */
    public static boolean inWhitelist(String method, String requestUri, List<String> rules) {
        requestUri = StrUtil.removePrefix(requestUri, "/");
        requestUri = StrUtil.removeSuffix(requestUri, "/");
        String[] splitUri = requestUri.split("/");
        for (String rule : rules) {
            // 是否配置了关闭网关验签的规则
            if (rule.equals("/")) {
                return true;
            }
            String[] splitRule = rule.split(":");
            String whiteMethod = splitRule[0].toUpperCase();
            String validMethod = method.toUpperCase();
            if (whiteMethod.equals(validMethod)) {
                String whiteUrl = splitRule[1];
                String[] splitUriRule = whiteUrl.split("/");
                if (splitUriRule.length != splitUri.length) {
                    continue;
                }
                if (whiteUrl.indexOf("{") > -1) {
                    // 按路径参数验证
                    if (matchPathVariable(splitUriRule, splitUri)) {
                        return true;
                    }
                } else {
                    // 按全匹配验证
                    if (whiteUrl.equals(requestUri)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private static boolean matchPathVariable(String[] pattern, String[] path) {
        int index = 0;
        for (; index < path.length; ) {
            if (pattern.length <= index) {
                return false;
            }
            if (pattern[index].indexOf("{") > -1) {
                if (!transType(pattern[index], path[index])) {
                    return false;
                }
            } else {
                if (!path[index].equals(pattern[index])) {
                    return false;
                }
            }
            index++;
        }
        return true;
    }

    private static boolean transType(String type, String value) {
        if (type.equals("{LONG}")) {
            try {
                Long.parseLong(value);
            } catch (NumberFormatException e) {
                return false;
            }
        } else if (type.equals("{STRING}")) {
            return true;
        }
        return true;
    }

    /**
     * 获取并验证Header中的参数，是否满足验签所需
     *
     * @param request
     * @return
     */
    private TreeMap<String, Object> getVerifyNecessaryHeader(HttpServletRequest request) throws MissingHeaderException {
        // 获取请求头
        TreeMap<String, Object> headerMap = RequestUtils.getRequestHeaderMap(request);
        if (Objects.isNull(headerMap.get(GatewayConstant.REQ_HEAD_PARAM_APP_ID)) ||
                Objects.isNull(headerMap.get(GatewayConstant.REQ_HEAD_PARAM_SIGN)) ||
                Objects.isNull(headerMap.get(GatewayConstant.REQ_HEAD_PARAM_SIGN_TYPE)) ||
                Objects.isNull(headerMap.get(GatewayConstant.REQ_HEAD_PARAM_CLIENT_TYPE)) ||
                Objects.isNull(headerMap.get(GatewayConstant.REQ_HEAD_PARAM_CLIENT_VERSION)) ||
                Objects.isNull(headerMap.get(GatewayConstant.REQ_HEAD_PARAM_TIMESTAMP))) {

            // 缺少请求头参数
            throw new MissingHeaderException();
        }

        if (StrUtil.isBlank(headerMap.get(GatewayConstant.REQ_HEAD_PARAM_APP_ID).toString()) ||
                StrUtil.isBlank(headerMap.get(GatewayConstant.REQ_HEAD_PARAM_SIGN).toString()) ||
                StrUtil.isBlank(headerMap.get(GatewayConstant.REQ_HEAD_PARAM_SIGN_TYPE).toString()) ||
                StrUtil.isBlank(headerMap.get(GatewayConstant.REQ_HEAD_PARAM_CLIENT_TYPE).toString()) ||
                StrUtil.isBlank(headerMap.get(GatewayConstant.REQ_HEAD_PARAM_CLIENT_VERSION).toString()) ||
                StrUtil.isBlank(headerMap.get(GatewayConstant.REQ_HEAD_PARAM_TIMESTAMP).toString())) {

            // 缺少请求头参数
            throw new MissingHeaderException();
        }
        return headerMap;
    }

    /**
     * 验证不同验签方式的正确性
     *
     * @return
     */
    private void handleSignRule(String method, TreeMap<String, Object> headerMap, String requestUri) throws SignRuleException {
        if (GatewayConstant.SIGN_TYPE_RSA.equals(headerMap.get(GatewayConstant.REQ_HEAD_PARAM_SIGN_TYPE))) {
            // 根据请求地址，判断是否允许使用RSA方式验签
            if (!inWhitelist(method, requestUri, signVerifynProperties.getPermitRsaUrls())) {
                throw new SignRuleException(CommonReturnCode.SIGN_RSA_NOT_PERMIT);
            }
        } else if (GatewayConstant.SIGN_TYPE_MD5.equals(headerMap.get(GatewayConstant.REQ_HEAD_PARAM_SIGN_TYPE))) {
            // 若MD5方式签名，验证请求头中必须传入userId
        } else {
            // 不支持的签名算法（仅支持MD5和RSA两种方式）
            throw new SignRuleException(CommonReturnCode.GATEWAY_SIGN_TYPE_FAIL);
        }
    }

    /**
     * 根据Method类型，获取需要验签的业务参数
     *
     * @return
     */
    private Pair<String, TreeMap<String, Object>> getVerifyParams(HttpServletRequest requestWrapper, String requestUri) throws IOException, RequestBodyFormatException {
        String newUri = requestUri;
        String method = requestWrapper.getMethod();


        SortedMap<String, Object> queryParam = RequestUtils.getAllParams(requestWrapper);

        //Map<String, String> paramMap = HttpUtil.decodeParamMap(queryParam, CharsetUtil.UTF_8);
        TreeMap<String, Object> params = new TreeMap<>(queryParam);

        if (HttpMethod.POST.matches(method) || HttpMethod.PUT.matches(method)) {
            Object attribute = requestWrapper.getAttribute(GatewayConstant.CACHE_REQUEST_BODY_OBJECT_KEY);
            if (Objects.nonNull(attribute)) {
                String cachedBody = String.valueOf(attribute);
                if (isJsonObject(cachedBody)) {
                    // 对象类型的Body参数，需将参数与Query参数合并
                    HashMap<String, String> jsonMap;
                    try {
                        jsonMap = JSON.parseObject(cachedBody, LinkedHashMap.class, Feature.OrderedField);
                    } catch (Exception e) {
                        throw new RequestBodyFormatException();
                    }
                    params.putAll(jsonMap);
                } else if (isJsonArray(cachedBody)) {
                    // 数组类型的Body参数，不参与签名
                    newUri += "";
                } else {
                    throw new RequestBodyFormatException();
                }
            }
        }
        return Pair.with(newUri, params);
    }

    /**
     * 判断JSON字符串是否是JSON对象
     *
     * @param json
     * @return
     */
    private boolean isJsonObject(String json) {
        return json.trim().startsWith("{");
    }

    /**
     * 判断JSON字符串是否是JSON数组
     *
     * @param json
     * @return
     */
    private boolean isJsonArray(String json) {
        return json.trim().startsWith("[");
    }

    /**
     * 处理验签
     *
     * @param requestUri
     * @param params
     * @param headers
     * @return
     * @throws ValidateTokenException
     * @throws ValidatePublicKeyException
     */
    private boolean verifySign(String requestUri, TreeMap<String, Object> params, TreeMap<String, Object> headers,HttpServletRequest request) throws ValidateTokenException, ValidatePublicKeyException {
        boolean pass = false;
        String sign = headers.get(GatewayConstant.REQ_HEAD_PARAM_SIGN).toString();
        String signType = headers.get(GatewayConstant.REQ_HEAD_PARAM_SIGN_TYPE).toString();

        // 去除不需要验签的请求头参数
        headers.remove(GatewayConstant.REQ_HEAD_PARAM_SIGN);

        // 判断签名方式，进行验签
        if (GatewayConstant.SIGN_TYPE_MD5.equals(signType)) {
            String token = request.getHeader(header);
            if (com.ruoyi.common.utils.StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
            {
                token = token.replace(Constants.TOKEN_PREFIX, "");
            }
            if (Objects.isNull(token)) {
                throw new ValidateTokenException();
            }
            // 待签名字符串：请求地址 + 请求参数 + token + 请求头参数
            String signStr = requestUri + this.handleSignParam(params) + token + this.handleSignParam(headers);
            String serverSign = SecureUtil.md5(signStr);
            pass = serverSign.equals(sign);

        } else if (GatewayConstant.SIGN_TYPE_RSA.equals(signType)) {

            String publicKey = appSecretProperties.getMasterPublicKey(headers.get(GatewayConstant.REQ_HEAD_PARAM_APP_ID).toString());
            if (publicKey == null) {
                throw new ValidatePublicKeyException();
            }
            // 初始化主密钥
            Sign signature = SecureUtil.sign(SignAlgorithm.MD5withRSA, null, publicKey);

            // 待签名字符串
            String signStr = requestUri + this.handleSignParam(params) + this.handleSignParam(headers);
            // 验签
            Base64.Decoder decoder = Base64.getDecoder();
            pass = signature.verify(signStr.getBytes(), decoder.decode(sign));
            // 若主密钥验签失败，且备用密钥存在的情况下（即开启了主备密钥切换），则使用备用密钥再次验签
            if (!pass) {
                publicKey = appSecretProperties.getBackupPublicKey(headers.get(GatewayConstant.REQ_HEAD_PARAM_APP_ID).toString());
                if (!StringUtils.isEmpty(publicKey)) {
                    signature = SecureUtil.sign(SignAlgorithm.MD5withRSA, null, publicKey);
                    pass = signature.verify(signStr.getBytes(), decoder.decode(sign));
                }
            }
        }
        return pass;
    }

    /**
     * 对需要签名的参数进行排序，并拼接为一个字符串
     *
     * @param params
     * @return
     */
    private String handleSignParam(SortedMap<String, Object> params) {
        StringBuilder builder = new StringBuilder();

        for (Map.Entry entry : params.entrySet()) {
            Object key = entry.getKey();
            Object value = entry.getValue();

            if (StringUtils.isEmpty((CharSequence) key) || value == null || StringUtils.isEmpty(JSONUtil.toJsonStr(value.toString()))) {
                // key或value为空，则不拼接至待签名字符串
                continue;
            }
            if("null".equals(value.toString())){
                continue;
            }
            if (value instanceof JSONArray) {
                continue;
            } else {
                if (GatewayConstant.NULL_STRING.equals(value)) {
                    // 值为空字符串（null），则不拼接至待签名字符串
                    continue;
                }
            }
            if(!isJsonObject(value.toString())){
                // 拼接至待签名字符串
                builder.append(key).append(value);
            }else {
                if("extra".equals(key)){
                    builder.append(key).append(value);
                }
            }

        }
        return builder.toString();
    }

    @SneakyThrows
    public void requestLogMessage(String requestUri, String httpMethod, String mediaType, HttpServletRequest request) {
        StringBuilder logMsg = new StringBuilder();
        logMsg.append("{");
        // 日志类型
        logMsg.append("\"日志类型\"").append(":").append("\"").append("接口调用记录").append("\"");
        logMsg.append(",");
        // ContentType
        logMsg.append("\"contentType\"").append(":").append("\"").append(mediaType).append("\"");
        logMsg.append(",");
        // Method
        logMsg.append("\"method\"").append(":").append("\"").append(httpMethod).append("\"");
        logMsg.append(",");
        // Headers
        logMsg.append("\"headers\"").append(":").append("\"").append(RequestUtils.getRequestHeaderMap(request)).append("\"");
        logMsg.append(",");
        // URI
        logMsg.append("\"uri\"").append(":").append("\"").append(requestUri).append("\"");
        logMsg.append(",");
        // QueryParam
        logMsg.append("\"queryParam\"").append(":").append("\"").append(request.getQueryString()).append("\"");
        // Body
        if (StrUtil.contains(mediaType,MediaType.APPLICATION_JSON_VALUE)) {
            logMsg.append(",");
            SortedMap<String, Object> queryParam = RequestUtils.getAllParams(request);
            logMsg.append("\"body\"").append(":").append(org.springframework.util.StringUtils.trimAllWhitespace(queryParam.toString()));
        }
        logMsg.append("}");
        log.info(logMsg.toString());
    }
}
